Where context meet detail
Master of Business Analytics, Monash University
2025-10-10
Traditional Map
Zoom-in Map
Traditional Map
ggplot(data = vic_fish) +
geom_sf(fill = "grey90", color = "grey10") +
geom_sf(data = conn_small, aes(alpha = weight), color = "black") +
geom_sf(data = hosp_points, color = "red", size = 1, alpha = 0.5) +
geom_sf(data = racf_points, color = "blue", size = 1, alpha = 0.5) +
labs(title = "Transportation between Hospital and Age Care Facilities in VIC
during COVID - 19") +
theme_map()Zoom-in Map
center_bbox <- st_transform(center_bbox, st_crs(vic_fish))
ggplot(data = vic_fish) +
geom_sf(fill = "grey90", color = "grey10") +
geom_sf(data = conn_small, aes(alpha = weight), color = "black") +
geom_sf(data = hosp_points, color = "red", size = 1, alpha = 0.5) +
geom_sf(data = racf_points, color = "blue", size = 1, alpha = 0.5) +
geom_sf_label(data = hosp_points, aes(label = hosp_name), color = "red", size = 2.5, nudge_x = -1300) +
geom_sf_label(data = racf_points, aes(label = racf_name), color = "blue", size = 2.5, nudge_x = -1500) +
coord_sf(xlim = center_bbox[c("xmin", "xmax")], ylim = center_bbox[c("ymin", "ymax")]) +
labs(title = "Transportation between Hospital and Age Care Facilities in VIC
during COVID - 19") +
theme_map()The transformation operates in polar coordinates:
Catersian Coordinate System
Polar Coordinate System
The transformation operates in polar coordinates:
\[ \begin{aligned} r &= \sqrt{(x - c_x)^2 + (y - c_y)^2} \\ \theta &= \arctan2(y - c_y, x - c_x) \end{aligned} \]
Then applies zone-specific radial mapping:
\[ r' = \begin{cases} \min(r \times z, r_{in}) & \text{if } r \leq r_{in} \text{ (focus)} \\ f_{compress}(r, s) & \text{if } r_{in} < r \leq r_{out} \text{ (glue)} \\ r & \text{if } r > r_{out} \text{ (context)} \end{cases} \]
where \(z\) = zoom_factor, \(s\) = squeeze_factor
Tip
All visualization functions use ggplot2 for easy customization
grid_df <- as_tibble(grid)
transform_df <- as_tibble(transform) |>
dplyr::mutate(
zone = attr(transform, "zones"),
r_orig = attr(transform, "original_radius"),
r_new = attr(transform, "new_radius")
)
arrows_df <- cbind(grid_df, transform_df)
ggplot() +
# Draw arrows showing movement
# Original points
geom_point(
data = grid_df, aes(x = x, y = y),
size = 0.6, alpha = 0.7, color = "black"
) +
# Transformed points
geom_point(
data = transform_df, aes(x = x_new, y = y_new, color = zone),
size = 0.6, alpha = 0.7
) +
geom_segment(
data = arrows_df |> filter(zone != "context"),
aes(x = x, y = y, xend = x_new, yend = y_new, color = zone),
arrow = arrow(length = unit(0.02, "npc")), # optional arrowhead
alpha = 0.6, size = 0.5
) +
scale_color_manual(values = c("focus" = "#c60000ff",
"glue" = "#141497ff",
"context" = "#FFCC00")) +
coord_equal() +
theme_minimal(base_size = 14) +
labs(title = "Fisheye Transformation: Point Movement", x = "x", y = "y")Tip
All visualization functions use ggplot2 for easy customization
Three-layer architecture:
Core Transformation Layer
Mathematical fisheye operations
Geospatial Integration Layer
sf/sfc object handling
Utility & Visualization Layer
Helper functions and plotting
Modular Design
Each layer is independent and can be used separately or combined for complete workflows.
┌────────────────────────────────────────────────────────┐
│ USER INPUT │
│ sf/sfc object + parameters │
└─────────────────────┬──────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ GEOSPATIAL INTEGRATION LAYER │
│ │
│ sf_fisheye() │
│ ├─ Auto CRS handling (EPSG:7855 or UTM) │
│ ├─ .resolve_center() - Parse center input │
│ ├─ Normalize coordinates to [-1,1] │
│ └─ Calls st_transform_custom() │
└─────────────────────┬──────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ GEOMETRY HANDLER LAYER │
│ │
│ st_transform_custom() │
│ ├─ Iterates through geometries │
│ ├─ Extracts coordinates │
│ ├─ Calls fisheye_fgc() for transformation │
│ ├─ Rebuilds geometries │
│ └─ Auto-closes polygon rings │
└─────────────────────┬──────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ CORE TRANSFORMATION LAYER │
│ │
│ fisheye_fgc() │
│ ├─ Cartesian → Polar conversion │
│ ├─ Zone classification (focus/glue/context) │
│ ├─ Radial transformation by zone │
│ ├─ Optional revolution (angular twist) │
│ └─ Polar → Cartesian conversion │
└─────────────────────┬──────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────┐
│ GEOSPATIAL INTEGRATION LAYER │
│ │
│ sf_fisheye() (continued) │
│ ├─ Denormalize coordinates back to map units │
│ └─ Restore original CRS │
└─────────────────────┬──────────────────────────────────┘
│
▼
TRANSFORMED sf/sfc OUTPUT
┌────────────────────────────────────────────────────────┐
│ OPTIONAL: UTILITY FUNCTIONS │
│ (for testing & visualization) │
│ │
│ • create_test_grid() - Generate test coordinates │
│ • classify_zones() - Zone classification helper │
│ • plot_fisheye_fgc() - Visualization function │
└────────────────────────────────────────────────────────┘melbourne <- vic |>
filter(LGA_NAME = "Melbourne")
vic_fish <- sf_fisheye(vic,
center = melbourne,
r_in = 0.34,
r_out = 0.5,
zoom_factor = 20)
vic_fish |>
ggplot() + geom_sf()Smart CRS Handling
Victoria region: EPSG:7855 (GDA2020 / MGA55)
Other regions: Auto-calculated UTM zones
Already projected: Uses existing CRS
library(purrr)
library(dplyr)
library(stringr)
zoom_seq <- seq(1, 20, by = 0.1)
center_pt_proj <- melbourne
# Calculate melbourne centroid once
melbourne_cent <- melbourne |> st_union() |> st_centroid()
fisheye_frames <- map_dfr(zoom_seq, function(z) {
# Apply fisheye transformations
vic_fish_new <- sf_fisheye(vic_fish, center = center_pt_proj,
r_in = 0.34, r_out = 0.5, zoom_factor = z)
conn_fish <- sf_fisheye(conn_small, center = center_pt_proj,
r_in = 1.12, r_out = 5, zoom_factor = z)
hosp_fish <- sf_fisheye(hosp_points, center = center_pt_proj,
r_in = 1.23, r_out = 2.3, zoom_factor = z)
racf_fish <- sf_fisheye(racf_points, center = center_pt_proj,
r_in = 1.58, r_out = 2.3, zoom_factor = z)
# Transform melbourne_cent to match coordinate system
melbourne_cent_trans <- melbourne_cent |> st_transform(st_crs(hosp_fish))
# Calculate distances
distances_racf <- st_distance(racf_fish, melbourne_cent_trans) |> as.numeric() |> round()
distances_hosp <- st_distance(hosp_fish, melbourne_cent_trans) |> as.numeric() |> round()
# Add distances and counts
racf_fish <- racf_fish |> mutate(dist = distances_racf) |> add_count(dist)
hosp_fish <- hosp_fish |> mutate(dist = distances_hosp) |> add_count(dist)
# Filter based on distance frequency
racf_fish_filter <- racf_fish |> filter(n != max(n))
hosp_fish_filter <- hosp_fish |> filter(n != max(n))
# Filter connections
conn_fish_filter <- conn_fish |> filter(source %in% racf_fish_filter$source)
conn_fish_filter <- conn_fish_filter |> arrange(desc(weight)) |> head(10)
# Filter based on connections
racf_fish_filter <- racf_fish_filter |> filter(source %in% conn_fish_filter$source)
hosp_fish_filter <- hosp_fish_filter |> filter(destination %in% conn_fish_filter$destination)
# Shorten hospital names
hosp_fish_filter <- hosp_fish_filter %>%
mutate(hosp_name_short = hosp_name %>%
str_remove(" Health Service$") %>%
str_remove(" Hospital$") %>%
str_remove(" Inc\\.$") %>%
str_remove(" Ltd\\.$") %>%
str_remove(" Private$") %>%
str_remove(" Centre$") %>%
str_remove(" Campus$") %>%
str_remove("^The ") %>%
str_replace(" Private Hospital", "") %>%
str_replace(" Health$", "") %>%
str_replace(" Rehabilitation Hospital", " Rehab") %>%
str_replace(" Rehabilitation Centre", " Rehab") %>%
str_replace(" Day Surgery", " Day Surg") %>%
str_replace(" District Hospital", "") %>%
str_replace(" Regional Hospital", " Regional") %>%
str_squish()
)
tibble(
zoom_factor = z,
vic = list(vic_fish_new),
conn = list(conn_fish_filter),
hosp = list(hosp_fish_filter),
racf = list(racf_fish_filter)
)
})
# Reshape to long format
fish_long <- map_dfr(1:nrow(fisheye_frames), function(i) {
z <- fisheye_frames$zoom_factor[i]
bind_rows(
fisheye_frames$vic[[i]] %>% mutate(type = "vic", zoom_factor = z),
fisheye_frames$conn[[i]] %>% mutate(type = "conn", zoom_factor = z),
fisheye_frames$hosp[[i]] %>% mutate(type = "hosp", zoom_factor = z),
fisheye_frames$racf[[i]] %>% mutate(type = "racf", zoom_factor = z)
)
})
library(gganimate)
library(ggplot2)
p <- ggplot() +
# VIC map Polygon
geom_sf(data = subset(fish_long, type == "vic"),
fill = NA, color = "grey") +
# Hospital labels (using short names)
geom_sf_label(data = subset(fish_long, type == "hosp"),
aes(label = hosp_name_short), color = "red", size = 2.5, alpha = 0.5) +
# RACF labels
geom_sf_label(data = subset(fish_long, type == "racf"),
aes(label = racf_name), color = "blue", size = 2.5, alpha = 0.5) +
# Hospitals (points)
geom_sf(data = subset(fish_long, type == "hosp"),
color = "red", size = 2) +
# Age Care Facilities (points)
geom_sf(data = subset(fish_long, type == "racf"),
color = "blue", size = 2) +
# Connections
geom_sf(data = subset(fish_long, type == "conn"),
aes(alpha = weight), color = "black") +
coord_sf(crs = st_crs(fish_long)) +
labs(title = "Transportation between Hospital and Age Care Facilities in Victoria during COVID-19",
subtitle = "Zoom: {current_frame}×") +
theme_map() +
theme(
plot.title = element_text(size = 15, hjust = 0.5, margin = margin(t = 10, b = 5)),
plot.subtitle = element_text(size = 10, hjust = 0.5),
plot.margin = margin(t = 20, r = 10, b = 10, l = 10)
) +
transition_manual(zoom_factor)
anim <- animate(
p,
fps = 25,
duration = 8,
width = 1366,
height = 768,
res = 150
)
anim_save("fisheye_zoom_gganimate.gif", animation = anim)Percentage of the total population
Data: Observatoire des Territoires
Source: Benjamin Nowak
Percentage of the total population
Data: Observatoire des Territoires
Source: Benjamin Nowak
Melbourne Road Map
Melbourne Road Map after Transform
| Domain | Application | Benefit |
|---|---|---|
| Urban Planning | CBD-focused regional maps | Detail downtown + suburban context |
| Transportation | Route & congestion analysis | Zoom bottlenecks + preserve network |
| Public Health | Disease outbreak mapping | Magnify hotspots + regional spread |
| Real Estate | Property visualization | Highlight listings + neighborhood |
| Emergency | Incident response | Detail at scene + surrounding resources |
| Data Viz | Network graphs | Focus on central nodes + topology |
Multi-focal fisheye
Blend multiple focus regions with weighted transitions
Temporal fisheye
Animate transformations over time-series data
3D extensions
Spherical and hemispherical projections
AI-driven centers
Automatic focus detection from data density
Interactive dashboards
Shiny apps with real-time parameter adjustment
Web mapping
Integration with leaflet/mapview
# Install from GitHub
devtools::install_github("Alex-Nguyen-VN/mapycusmaximus")
# Load package
library(mapycusmaximus)
library(sf)
# Quick example
data <- st_read("your_data.shp")
result <- sf_fisheye(
data,
center = c(lon, lat),
center_crs = "EPSG:4326",
r_in = 0.34,
r_out = 0.5,
zoom_factor = 1.5
)
# Plot
ggplot() + geom_sf(data = result)fisheye_fgc() : Core transformationsf_fisheye() : Geospatial wrapperst_transform_custom() : Geometry handlerplot_fisheye_fgc() : VisualizationContributions Welcome!
Open source project seeking collaborators for enhancements and use cases
“A cartographic lens to see both detail and context – at once”
Contact Information
📧 thanhcuong10091992@gmail.com
🔗 github.com/Alex-Nguyen-VN/mapycusmaximus
method = "expand"Bidirectional expansion in glue zone:
r_inr_outst_transform_custom()Handles all standard sf geometry types:
| Type | Support | Notes |
|---|---|---|
| POINT | ✅ | Direct coordinate transform |
| LINESTRING | ✅ | Preserves vertex order |
| POLYGON | ✅ | Auto-closes rings |
| MULTIPOLYGON | ✅ | Handles multiple parts & holes |
Key Feature
Polygon rings are automatically re-closed after transformation to ensure first vertex = last vertex.
fisheye_fgc.RPrimary function: fisheye_fgc()
Purpose: Pure mathematical transformation
Key operations:
Converts Cartesian to Polar coordinates
Applies zone-specific radial mapping
Returns transformed coordinates
Input: Numeric matrix (x, y)
Output: Transformed matrix + metadata
fisheye_fgc <- function(coords, cx = 0, cy = 0,
r_in = 0.34, r_out = 0.5,
zoom_factor = 1.5,
squeeze_factor = 0.3,
method = "expand",
revolution = 0.0) {
# Convert to polar coordinates
dx <- coords[, 1] - cx
dy <- coords[, 2] - cy
radius <- sqrt(dx^2 + dy^2)
angle <- atan2(dy, dx)
# Classify into zones
zone <- ifelse(radius <= r_in, "focus",
ifelse(radius <= r_out, "glue", "context"))
# Apply transformations...
# Returns: matrix with x_new, y_new
}sf_fisheye.RPrimary function: sf_fisheye()
Purpose: Bridge between sf objects and fisheye transformation
Responsibilities:
Automatic CRS detection & projection
Center resolution (lon/lat, normalized, sf geometry)
Coordinate normalization/denormalization
Preserve aspect ratio handling
sf_related.RPrimary function: st_transform_custom()
Purpose: Generic coordinate transformer for sf geometries
Handles:
POINT, LINESTRING, POLYGON, MULTIPOLYGON
Automatic ring closure for polygons
Per-geometry error handling
Preserves CRS and attributes
Smart CRS Handling
Victoria region: EPSG:7855 (GDA2020 / MGA55)
Other regions: Auto-calculated UTM zones
Already projected: Uses existing CRS
utils.R - Helper FunctionsTesting & Setup
create_test_grid()
Generates regular coordinate grids
Useful for transformation testing
Customizable spacing and range
Zone Classification
classify_zones()
Assigns points to focus/glue/context
Used for visualization
Helpful for analysis
Visualization
plot_fisheye_fgc()
Side-by-side comparison
Color-coded by zone
Shows boundary circles - Built on ggplot2
User-facing:
sf_fisheye(): Main entry point
fisheye_fgc():Core math
Internal:
st_transform_custom(): Geometry handler
.resolve_center():Center parser
Key Design Principle
The core transformation (fisheye_fgc) is independent of sf, allowing use in non-geospatial contexts.